#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Author: wangluofan

import os
import sys
import time
import threading
import subprocess

from enum import Enum
from typing import Optional

repos = ['iOS-SMBase', 'iOS-SMSDK', 'iOS-GTSDK', 'iOS-SMIMSystem']

class GitCmdType(Enum):
    unknown = 0x00
    switch  = 0x01
    commit  = 0x02
    push    = 0x04
    merge   = 0x08
    exec    = 0x10
    
class TerminalColor:
    RESET = "\033[0m"
    
    BLACK = "\033[30m"
    RED = "\033[31m"
    GREEN = "\033[32m"
    YELLOW = "\033[33m"
    BLUE = "\033[34m"
    MAGENTA = "\033[35m"
    CYAN = "\033[36m"
    WHITE = "\033[37m"

class GitCmd:
    class GitCmdCounter:
        __slots__ = ['stop_event', 'thread']
        
        def __init__(self) -> None:
            self.stop_event: Optional[threading.Event] = None
            self.thread: Optional[threading.Thread] = None
            
        def stop(self):
            if not self.stop_event:
                return
            self.stop_event.set()
            
        def count(self):
            if not self.stop_event:
                return
            
            start_time = time.time()
            while not self.stop_event.is_set():
                elasped = time.time() - start_time
                hours, reminder = divmod(elasped, 3600)
                minutes, seconds = divmod(reminder, 60)
                
                time_str = f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
                
                sys.stdout.write(f"\r{TerminalColor.CYAN}任务执行中... 已耗时: {time_str}{TerminalColor.RESET}")
                sys.stdout.flush()
                time.sleep(1)
                
            print()
            print()
                
        def run(self) -> 'GitCmd.GitCmdCounter':
            self.stop_event = threading.Event()
            self.thread = threading.Thread(target=self.count)
            self.thread.start()
            
            return self
        
    class GitCmdOperation:
        def check_env(self) -> bool:
            ret = subprocess.call(['which', '-s', 'git'], shell=False)
            if ret:
                print(f'{TerminalColor.RED}git: no command found{TerminalColor.RESET}')
                return False
            
            return True
        
        def fetch_remote_branchs(self, branch: Optional[str], filter: bool = False) -> Optional[list[str]]:
            if filter and not branch:
                return None
            self.fetch()

            pipe =  subprocess.Popen(['git', 'branch', '--remotes'], shell=False, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            if filter and branch:
                pipe = subprocess.Popen(['grep', branch], shell=False, encoding='utf-8', stdin=pipe.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            
            stdout, stderr = pipe.communicate()
            
            if pipe.returncode:
                print(f'{TerminalColor.RED}{stderr}{TerminalColor.RESET}')
                return None
            
            b_list = stdout.split('\n')
            branches = []
            
            for b in b_list:
                if len(b) <= 0:
                    continue
                branches.append(b.strip())
            
            return branches
        
        def fetch_local_branches(self) -> Optional[list[str]]:
            p = subprocess.Popen(['git', 'branch', '--no-color'], shell=False, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            stdout, stderr = p.communicate()
            
            if p.returncode:
                print(f'{TerminalColor.RED}{stderr}{TerminalColor.RESET}')
                return None
            
            b_list = stdout.split('\n')
            branches = []
            
            for b in b_list:
                if len(b) <= 0:
                    continue
                if b[0] == '*':
                    branches.append(b[1:].strip())
                    continue
                branches.append(b.strip())
            
            return branches
        
        def commit(self, message: Optional[str]) -> bool:
            if message is None or len(message) <= 0:
                print(f'{TerminalColor.RED}git: message should not empty{TerminalColor.RESET}')
                return False

            print(f'{TerminalColor.GREEN}adding all changes.{TerminalColor.RESET}')
            ret = subprocess.call(['git', 'add', '.'], shell=False)
            if ret:
                print(f'{TerminalColor.RED}git add failed{TerminalColor.RESET}')
                return False

            p = subprocess.Popen(['git', 'status', '--porcelain'], shell=False, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            stdout, _ = p.communicate()
            if p.returncode or len(stdout) <= 0:
                return True
            
            ret = subprocess.call(['git', 'commit', '-m', message], shell=False)
            if ret:
                return False
            
            return True
        
        def pull(self) -> bool:
            branch = self.current()
            if not branch:
                return False

            rems = self.fetch_remote_branchs(branch, filter=True)
            if not rems or len(rems) < 1:
                return False
            
            ret = subprocess.call(['git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], shell=False)
            if ret:
                ret = subprocess.call(['git', 'branch', f'--set-upstream-to=origin/{branch}'], shell=False)
                
            if ret:
                return False
            
            print(f'{TerminalColor.GREEN}pulling remote changes{TerminalColor.RESET}')
            
            ret = subprocess.call(['git', 'pull'], shell=False)
            
            return False if ret else True

        def current(self) -> Optional[str]:
            p = subprocess.Popen(['git', 'branch', '--show-current'], encoding='utf-8', stdout=subprocess.PIPE, shell=False, text=True)
            stdout, stderr = p.communicate()
            
            if p.returncode:
                print(stderr)
                return None
            
            return stdout.strip()
        
        def execute(self) -> bool:
            if len(sys.argv) < 3:
                return False
            
            args = [sys.argv[i] for i in range(2, len(sys.argv))]
            
            try:
                p = subprocess.call(args, shell=False)
            except Exception as e:
                print(f'{TerminalColor.RED}Invalid command{TerminalColor.RESET}')
                return False
                
            return False if p else True
        
        def switch(self, branch: Optional[str]) -> bool:
            if branch is None:
                print(f'{TerminalColor.RED}Invalid argument [branch]{TerminalColor.RESET}')
                return False
            
            print(f'{TerminalColor.GREEN}Switching to {branch}{TerminalColor.RESET}')
            
            cb = self.current()
            if cb and cb.strip() == branch:
                return self.pull()

            local_branches = self.fetch_local_branches()
            
            ret = 0
            if local_branches and branch in local_branches:
                ret = subprocess.call(['git', 'checkout', branch], shell=False)
            else:
                rems = self.fetch_remote_branchs(branch, filter=True)
                if not rems or len(rems) < 1 or f'origin/{branch}' not in rems:
                    ret = subprocess.call(['git', 'checkout', '-b', branch], shell=False)
                else:
                    counter = GitCmd.GitCmdCounter().run()
                    ret = subprocess.call(['git', 'checkout', '-b', branch, '--track', f'origin/{branch}'], shell=False)
                    counter.stop()
                
            if ret:
                print(f'{TerminalColor.RED}Switching to {branch} failed{TerminalColor.RESET}')
                return False
            
            self.pull()
            return True
        
        def push(self) -> bool:
            cb = self.current()
            if cb is None or len(cb) <= 0:
                return False

            ret = subprocess.call(['git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], shell=False)
            if ret:
                ret = subprocess.call(['git', 'push', '--set-upstream','origin', cb], shell=False)
            else:
                ret = subprocess.call(['git', 'push'], shell=False)
                
            if ret:
                return False
            
            return True
        
        def fetch(self) -> bool:
            ret = subprocess.call(['git', 'fetch', '--all', '--depth=1'], shell=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            
            if ret:
                return False
            
            return True
        
        def merge(self, branch: Optional[str]) -> bool:
            if branch is None or len(branch) <= 0:
                return False
            
            cb = self.current()
            if not cb or len(cb) <= 0:
                return False
            
            ret = subprocess.call(['git', 'merge', '-no-ff', branch], shell=False)
            if ret:
                return False
            
            return True
    
    def perform(self):
        cmd: Optional[GitCmdType] = self._parse_args()
        if not cmd or cmd == GitCmdType.unknown:
            return

        op = GitCmd.GitCmdOperation()
        if op.check_env() == False:
            return
        
        global repos
        if not repos or len(repos) <= 0:
            return
        
        curp = os.getcwd()
        paths = os.listdir(os.curdir)
        
        for repo in repos:
            if repo in paths:
                os.chdir(os.path.join(curp, repo))
                print(f'{TerminalColor.MAGENTA}processing repo {repo}:{TerminalColor.RESET}')
        
                if cmd == GitCmdType.commit:
                    msg = input('Input Commit Message:')
                    op.commit(msg)
                if cmd == GitCmdType.exec:
                    if len(sys.argv) < 3:
                        print(f'{TerminalColor.RED}Missing arguments [cmd]{TerminalColor.RESET}')
                        return
                    op.execute()
                if cmd == GitCmdType.switch:
                    if len(sys.argv) < 3:
                        print(f'{TerminalColor.RED}Missing argument [branch]{TerminalColor.RESET}')
                        return
                    op.switch(sys.argv[2])
                if cmd == GitCmdType.push:
                    op.push()
                if cmd == GitCmdType.merge:
                    if len(sys.argv) < 3:
                        print(f'{TerminalColor.RED}Missing argument [branch]{TerminalColor.RESET}')
                        return
                    op.merge(sys.argv[2])
        
    def _check_args_valid(self, cmd_type: int,
                         should_has_branch_name: bool) -> bool:
        if cmd_type == GitCmdType.unknown:
            print('unkown command combination')    
            return False
        
        if should_has_branch_name == True and len(sys.argv) < 3:
            print('missing parameter [branch_name]')
            return False
        
        return True
    
    def _parse_args(self) -> Optional[GitCmdType]:
        if len(sys.argv) < 2:
            self._print_usage()
            return None
        
        cmd_type = GitCmdType.unknown.value
        should_has_branch_name = False

        cmd = sys.argv[1]
        if cmd == 'c':
            cmd_type = GitCmdType.commit.value
        if cmd == 'e':
            cmd_type = GitCmdType.exec.value
        elif cmd == 's':
            cmd_type = GitCmdType.switch.value
            should_has_branch_name = True
        elif cmd == 'p':
            cmd_type = GitCmdType.push.value
        elif cmd == 'm':
            cmd_type = GitCmdType.merge.value
            should_has_branch_name = True
        elif cmd == 'h':
            self._print_usage()
            return None
                
        if not self._check_args_valid(cmd_type, should_has_branch_name):
            self._print_usage()
            return None
        
        return GitCmdType(cmd_type)
        
    def _print_usage(self):
        print(f'{sys.argv[0]}: -[params]')
        print('    c: [message]: commit changes')
        print('    e: [cmd]: execute cmd on each repo')
        print('    s: [branch_name]: switch/create to a branch existed')
        print('    p: push changes to remote')
        print('    m: [branch_name]: merge branch specified')
        print('    h: show this menu')

if __name__ == '__main__':
    GitCmd().perform()
